.. _глава_53_start:

Перегрузка операторов
=====================

.. index::
  overloading
  перегрузка
  перегрузка операторов

Темы, рассмотренные в этой главе, в основном применимы и к классам. Самое большое
разница заключается в том, что поведение операции присваивания opAssign() н
не может быть перегружен для классов.
Перегрузка операторов включает в себя множество концепций, некоторые из которых будут рассмотрены
позже в книге (шаблоны, auto ref и т. д.). По этой причине эта глава может показаться вам
глава покажется вам сложнее, чем предыдущие.
Перегрузка операторов позволяет определить, как ведут себя пользовательские типы при
при использовании с операторами. В данном контексте термин "перегрузка" означает предоставление
определение оператора для конкретного типа.
В предыдущих главах мы рассмотрели, как определять структуры и их функции-члены.
главах. В качестве примера мы определили функцию-член increment(), чтобы
чтобы иметь возможность добавлять объекты Duration к объектам TimeOfDay. Вот две структуры
из предыдущих глав, но только те части, которые имеют отношение к этой главе


.. code-block:: d

  struct Duration
  {
      int minute;
  }

  struct TimeOfDay
  {
      int hour;
      int minute;
      void increment(Duration duration)
      {
          minute += duration.minute;
          hour += minute / 60;
          minute %= 60;
          hour %= 24;
      }
  }

  void main()
  {
      auto lunchTime = TimeOfDay(12, 0);
      lunchTime.increment(Duration(10));
  }

Преимуществом функций-членов является возможность определять операции типа
наряду с переменными-членами этого типа.
Несмотря на свои преимущества, функции-члены могут рассматриваться как ограниченные
по сравнению с операциями над фундаментальными типами. В конце концов, фундаментальные типы могут
могут легко использоваться с операторами:

.. code-block:: d

    int weight = 50;
    weight += 10; // by an operator

Согласно тому, что мы видели до сих пор, подобные операции могут быть выполнены только с помощью
функций-членов для пользовательских типов:

.. code-block:: d

    auto lunchTime = TimeOfDay(12, 0);
    lunchTime.increment(Duration(10)); // by a member function

Перегрузка операторов позволяет также использовать структуры и классы с операторами. Для
например, если предположить, что оператор += определен для TimeOfDay, то операцию
приведенная выше, может быть записана точно так же, как и в случае с фундаментальными типами:

.. code-block:: d

    lunchTime += Duration(10); // by an operator
    // (even for a struct)

Прежде чем перейти к деталям перегрузки операторов, давайте сначала посмотрим, как строка
выше будет включена для TimeOfDay. Для этого необходимо переопределить функцию-член
increment() под специальным именем opOpAssign(string
op), а также указать, что это определение предназначено для символа +. Как будет
ниже, это определение фактически соответствует оператору +=.
Определение этой функции-члена не похоже на те, что мы видели до сих пор.
видели до сих пор. Это связано с тем, что opOpAssign на самом деле является шаблоном функции. Поскольку мы
будем рассматривать шаблоны в более поздних главах, мне придется попросить вас принять
синтаксис перегрузки операторов в его нынешнем виде:

.. code-block:: d

    struct TimeOfDay
    {
        // ...
        ref TimeOfDay opOpAssign(string op)(Duration duration) // (1)
        if (op == "+")
        { // (2)
            minute += duration.minute;
            hour += minute / 60;
            minute %= 60;
            hour %= 24;
            return this;
        }
    }


Определение шаблона состоит из двух частей:
#. opOpAssign(string op): Эта часть должна быть написана как есть и по сути это имя функции
Ниже мы увидим, что существуют и другие
функции-члены в дополнение к opOpAssign.
#. if (op == "+"): opOpAssign используется для перегрузки более чем одного оператора.
"+" указывает, что это операторная перегрузка, соответствующая символу +
символ. Этот синтаксис представляет собой ограничение шаблона, которое также будет рассмотрено в
последующих главах.
Также обратите внимание, что на этот раз тип возвращаемого значения отличается от типа возвращаемого значения функции-члена
increment(): Это уже не void. Мы обсудим
типы возврата операторов ниже.
За кулисами компилятор заменяет использование оператора += вызовами
к функции-члену opOpAssign!"+":

.. code-block:: d

    lunchTime += Duration(10);
    // The following line is the equivalent of the previous one
    lunchTime.opOpAssign!"+"(Duration(10));

Часть !"+", стоящая после opOpAssign, указывает, что этот вызов предназначен для определения
оператора для символа +. Мы рассмотрим этот синтаксис шаблона в последующих
главах.
Обратите внимание, что определение оператора, соответствующего +=, задается через "+", а не
а не "+=". Assign в имени opOpAssign() уже подразумевает, что это
что это имя оператора присваивания.
Возможность определять поведение операторов накладывает ответственность: на программиста.
Программист должен соблюдать ожидания. В качестве крайнего примера можно привести предыдущий оператор
оператор можно было определить так, чтобы он уменьшал значение времени. Однако люди, читающие код, все равно ожидали бы, что значение
будет увеличено оператором +=.
В некоторой степени можно свободно выбирать и типы операторов возврата. Тем не менее,
общие ожидания должны быть соблюдены и для типов возврата.
Помните, что операторы, которые ведут себя неестественно, могут привести к путанице
и ошибкам.

Перегружаемые операторы
------------------------

Существуют различные виды операторов, которые могут быть перегружены

**Унарные операторы**

Оператор, принимающий один операнд, называется унарным оператором:

.. code-block:: d

    ++weight;

++ - это унарный оператор, поскольку он работает с одной переменной.
Унарные операторы определяются функциями-членами с именем opUnary. opUnary
не принимает никаких параметров, потому что использует только объект, на котором выполняется оператор
на котором выполняется оператор.
Перегружаемые унарные операторы и соответствующие им строки операторов
следующие:

.. list-table:: Title
   :widths: 25 25 50
   :header-rows: 1
   :align: center

   * - Оператор
     - Описание
     - Строка
   * - -object
     - получить отрицательное значение
     - "-"
   * - +object
     - получить положительное значение
     - "+"
   * - ~object
     - битовая инверсия
     - "~"
   * - \*object
     - разыменование
     - "*"
   * - ++object
     - инкремент
     - "++"
   * - --object
     - декремент
     - "--"

Например, оператор ++ для Duration может быть определен следующим образом:

.. code-block:: d

    struct Duration
    {
        int minute;
        ref Duration opUnary(string op)() if (op == "++")
        {
            ++minute;
            return this;
        }
    }


Обратите внимание, что возвращаемый тип оператора здесь также отмечен как ref. Это будет
будет объяснено ниже.
Объекты Duration теперь можно увеличивать с помощью ++:

.. code-block:: d

    auto duration = Duration(20);
    ++duration;

Операторы post-increment и post-decrement не могут быть перегружены. Операторы
object++ и object-- используются компилятором автоматически, сохраняя предыдущее значение объекта.
сохранения предыдущего значения объекта. Например, компилятор применяет
эквивалент следующего кода для пост-инкремента:

.. code-block:: d

    // The previous value is copied by the compiler  automatically: */
    Duration __previousValue__ = duration;
    // The ++ operator is called: */
    ++duration;
    // Then __previousValue__ is used as the value of the post-increment operation. */

В отличие от некоторых других языков, в D копирование внутри пост-инкремента не имеет стоимости, если
значение выражения пост-инкремента фактически не используется. Это происходит потому, что
компилятор заменяет такие пост-инкрементные выражения их пре-инкрементными
аналогами:

.. code-block:: d

    // The value of the expression is not used below. The
     * only effect of the expression is incrementing 'i'. */
     i++;

Кроме того, если перегрузка opBinary поддерживает использование duration += 1, то
opUnary не нужно перегружать для ++duration и duration++. Вместо этого
компилятор использует выражение duration += 1 за кадром. Аналогично, перегрузка
duration -= 1 перегрузка охватывает также использование --duration и duration--.

**Бинарные операторы**

Оператор, принимающий два операнда, называется бинарным оператором

.. code-block:: d

    totalWeight = boxWeight + chocolateWeight;

В строке выше есть два отдельных двоичных оператора: оператор +, который складывает
значения двух операндов, которые находятся по обе стороны от него, и оператор =, который
присваивает значение правого операнда левому операнду.
В крайнем правом столбце ниже описана категория каждого оператора. Те, которые
отмеченные как "=", присваивают объекту левой стороны.


.. list-table:: Title
    :widths: 25 25 50 25 25
    :header-rows: 1
    :align: center

    * - Оператор
      - Описание
      - Функция
      - Функция для правой части
      - Категория

    * - \+
      - сложение
      - opBinary
      - opBinaryRight
      - arithmetic
    * - \-
      - вычитание
      - opBinary
      - opBinaryRight
      - arithmetic
    * - \*
      - умножение
      - opBinary
      - opBinaryRight
      - arithmetic
    * - \/
      -
      - opBinary
      - opBinaryRight
      - arithmetic
    * - %
      - взятие остатка
      - opBinary
      - opBinaryRight
      - arithmetic
    * - ^^
      - возведение в степень
      - opBinary
      - opBinaryRight
      - arithmetic
    * - &
      - побитовое И
      - opBinary
      - opBinaryRight
      - bitwise
    * - \|
      - побитовое ИЛИ
      - opBinary
      - opBinaryRight
      - bitwise
    * - ^
      - исключающие ИЛИ
      - opBinary
      - opBinaryRight
      - bitwise
    * - <<
      - сдвиг влево
      - opBinary
      - opBinaryRight
      - bitwise
    * - >>
      - сдвиг вправо
      - opBinary
      - opBinaryRight
      - bitwise
    * - >>>
      - беззнаковый сдвиг вправо
      - opBinary
      - opBinaryRight
      - bitwise
    * - ~
      - конкатенация
      - opBinary
      - opBinaryRight
      -
    * - in
      - содержится в
      - opBinary
      - opBinaryRight
      -
    * - ==
      - равно
      - opEqual
      -
      - logical
    * - !=
      - не равно
      - opEqual
      -
      - logical
    * - <
      - меньше
      - opCmp
      -
      - sorting
    * - <=
      - меньше равно
      - opCmp
      -
      - sorting
    * - >
      - больше
      - opCmp
      -
      - sorting
    * - >=
      - больше равно
      - opCmp
      -
      - sorting
    * - =
      - присваивание
      - opAssign
      -
      - =
    * - +=
      - инкремент
      - opOpAssign
      -
      - =
    * - -=
      - декремент
      - opOpAssign
      -
      - =
    * - \*=
      - умножить и присвоить
      - opOpAssign
      -
      - =
    * - \/=
      - разделить и присвоить
      - opOpAssign
      -
      - =
    * - %=
      - взять остаток и присвоить
      - opOpAssign
      -
      - =
    * - ^^=
      - возвести в степень и присвоить
      - opOpAssign
      -
      - =
    * - &=
      - побитовое И и присвоение
      - opOpAssign
      -
      - =
    * - \|=
      - побитовое ИЛИ и присвоение
      - opOpAssign
      -
      - =
    * - ^=
      - исключающие ИЛИ и присвоение
      - opOpAssign
      -
      - =
    * - <<=
      - сдвиг влево и присвоение
      - opOpAssign
      -
      - =
    * - >>=
      -  сдвиг вправо и присвоение
      -  opOpAssign
      -
      -  =
    * - >>>=
      - беззнаковый сдвиг вправо и присвоение
      - opOpAssign
      -
      - =
    * - ~
      - конкатенация
      - opOpAssign
      -
      - =


opBinaryRight предназначен для случаев, когда объект может появиться в правой части
оператора. Предположим, что бинарный оператор, который мы назовем op, появляется в
программе:

.. code-block:: d

    x op y

Чтобы определить, какую функцию-член вызвать, компилятор рассматривает
следующие два варианта:

.. code-block:: d

  // the definition for x being on the left:
  x.opBinary!"op"(y);
  // the definition for y being on the right:
  y.opBinaryRight!"op"(x);

Компилятор выбирает тот вариант, который подходит лучше других.
opBinaryRight полезен при определении арифметических типов, которые обычно
работают по обе стороны оператора, например, int работает:

.. code-block:: d

  auto x = MyInt(42);
  x + 1; // будет вызвана opBinary!"+"
  1 + x; // будет вызвана opBinaryRight!"+"

Еще одно распространенное применение opBinaryRight - оператор in. Обычно
имеет смысл определить opBinaryRight для объекта, который появляется с правой
стороне оператора in. Пример этого мы рассмотрим ниже.
Имя параметра rhs, встречающееся в следующих определениях, является сокращением от
правая сторона. Оно обозначает операнд, который появляется в правой части оператора
оператора:

.. code-block:: d

  x op y

В приведенном выше выражении параметр rhs будет представлять переменную y

Операторы индексации и срезов (slice)  элементов
-------------------------------------------------

Следующие операторы позволяют использовать тип как коллекцию элементов:

.. list-table:: Title
   :widths: 50 50 50
   :header-rows: 1
   :align: center

   * - Описание
     - Функция
     - Пример использования
   * - Доступ к элементу
     - opIndex
     - collection[i]
   * - присвоить элементу значение
     - opIndexAssign
     - collection[i] = 7
   * - унарная операция над элементом
     - opIndexUnary
     - ++collection[i]
   * - выполнение опреации и присвоение
     - opIndexOpAssign
     - collection[i] \*= 2
   * - количество элементов в коллекции
     - opDollar
     - collection[$ - 1]
   * - получение среза
     - opSlice
     - collection[]
   * - получение среза ограниченная индексами
     - opSlice(size_t, size_t)
     - collection[i..j]

Мы рассмотрим эти операторы ниже.
Следующие функции операторов взяты из ранних версий D. Они
не рекомендуется:

.. list-table:: Title
   :widths: 50 50 50
   :header-rows: 1
   :align: center

   * - Описание
     - Функция
     - Пример использования
   * - Унарная операция над всеми элементами коллекции
     - opSliceUnary
     - ++collection
   * - Унарная операция над некоторыми элементами коллекции
     - opSliceUnary
     - collection[i..j]
   * - присвоение значения всем элементам коллекции
     - opSliceAssign
     - collection[] = 42
   * - присвоение значения некоторым элементам коллекции
     - opSliceAssign
     - collection[i..j] = 7
   * - выполнить операцию над всеми элементами коллекции затем присвоить
     - opSliceOpAssign
     - collection[] \*=2
   * - выполнить операцию над некоторыми элементами коллекции затем присвоить
     - opSliceOpAssign
     - collection[i..j] \*= 2


**Прочие опрераторы**

Следующие операторы также могут быть перегружены:

.. list-table:: Title
   :widths: 50 50 50
   :header-rows: 1
   :align: center

   * - Описание
     - Функция
     - Пример использования
   * - Вызов функции
     - opCall
     - object(42)
   * - Преобразования типа
     - opCast
     - to!int("42")
   * - Диспетчеризация несуществующей функции
     - opDispatch
     - object.nonExistent()


Эти операторы будут описаны ниже в отдельных разделах.

Одновременное определение более чем одного оператора
----------------------------------------------------

Чтобы сделать примеры кода короткими, мы использовали только операторы ++, + и +=.
выше. Можно предположить, что когда один оператор перегружен для типа, многие
другие также должны быть перегружены. Например, операторы -- и -=
также определены для следующего типа Duration:


.. code-block:: d

    struct Duration
    {
        int minute;
        ref Duration opUnary(string op)() if (op == "++")
        {
            ++minute;
            return this;
        }

        ref Duration opUnary(string op)() if (op == "--")
        {
            --minute;
            return this;
        }
    }



В приведенных выше перегрузках операторов есть дублирование кода. Единственные различия
между похожими функциями выделены. Подобные дублирования кода можно
сократить, а иногда и вовсе избежать с помощью строковых миксинов. Мы познакомимся с ключевым словом mixin
в одной из последующих глав. Я хотел бы вкратце показать, как это ключевое слово
помогает при перегрузке операторов.
mixin вставляет указанную строку в исходный код прямо в том месте, где появляется оператор mixin
появляется в коде. Следующая структура эквивалентна приведенной выше:


.. code-block:: d

    struct Duration
    {
        int minute;
        ref Duration opUnary(string op)() if ((op == "++") || (op == "--"))
        {
            mixin(op ~ "minute;");
            return this;
        }

        ref Duration opOpAssign(string op)(int amount) if ((op == "+") || (op == "-"))
        {
            mixin("minute " ~ op ~ "= amount;");
            return this;
        }
    }


Если объекты Duration также нужно умножать и делить на сумму, достаточно добавить еще два условия.
необходимо добавить еще два условия к ограничению шаблона:

.. code-block:: d

     struct Duration
      {
          // ...
          ref Duration opOpAssign(string op)(int amount)if ((op == "+") || (op == "-") || (op == "*") || (
                  op == "/"))
          {
              mixin("minute " ~ op ~ "= amount;");
              return this;
          }
      }

      unittest
      {
          auto duration = Duration(12);
          duration *= 4;
          assert(duration.minute == 48);
          duration /= 2;
          assert(duration.minute == 24);
      }


На самом деле, ограничения шаблона являются необязательными:

.. code-block:: d

  ref Duration opOpAssign(string op)(int amount)
    // no constraint */
  {
    mixin ("minute " ~ op ~ "= amount;");
    return this;
  }

Типы возврата операторов
------------------------

При перегрузке оператора рекомендуется обратить внимание на возвращаемый тип
того же оператора на фундаментальных типах. Это поможет разобраться в коде
и уменьшить путаницу.
Ни один из операторов на фундаментальных типах не возвращает void. Этот факт должен быть
очевидным для некоторых операторов. Например, результат сложения двух значений int в виде
a + b является int:

.. code-block:: d

  int a = 1;
  int b = 2;
  int c = a + b; // c gets initialized by the return value
                 // of the + operator

Возвращаемые значения некоторых других операторов могут быть не столь очевидны. Например,
даже у таких операторов, как ++i, есть значения:

.. code-block:: d

  int i = 1;
  writeln(++i); // печатает 2

Оператор ++ не только увеличивает i, но и выдает новое значение i.
Кроме того, значение, получаемое с помощью ++, - это не просто новое значение i, а скорее
сама переменная і. Мы можем убедиться в этом, распечатав адрес результата этого
выражения:

.. code-block:: d

  int i = 1;
  writeln("The address of i                : ", &i);
  writeln("The address of the result of ++i: ", &(++i));

Выходные данные содержат одинаковые адреса:

.. code-block:: console

    Адрес i : 7FFF39BFEE78
    Адрес результата ++i: 7FFF39BFEE78

Я рекомендую соблюдать следующие правила при перегрузке
операторов для ваших собственных типов:


* Операторы, изменяющие объект
  За исключением opAssign, рекомендуется, чтобы операторы, которые
  модифицируют объект, возвращают сам объект. Это правило было соблюдено
  выше с операторами TimeOfDay.opOpAssign!"+" и Duration.opUnary!"++".
  Следующие два шага позволяют вернуть сам объект:

  #. Возвращаемый тип - это тип структуры, обозначенный ключевым словом ref, означающим
     означающим ссылку.
  #. Выход из функции осуществляется через return this, что означает возврат этого объекта.

  Операторами, модифицирующими объект, являются opUnary!"++", opUnary!"--" и все
  перегрузки opOpAssign.
* Логические операторы
  opEquals, который представляет == и !=, должен возвращать bool. Хотя оператор in
  обычно возвращает содержащийся объект, он может просто возвращать bool.
* Операторы сортировки
  opCmp, представляющий <, <=, > и >=, должен возвращать int.
* Операторы, создающие новый объект
  Некоторые операторы должны создавать и возвращать новый объект:

    * Двоичные операторы -, + и ~; и двоичный оператор ~
    * Арифметические операторы +, -, \*, /, % и ^^
    * Побитовые операторы &, \|, ^, <<, >> и >>>

Как было показано в предыдущей главе, opAssign возвращает копию этого
объекта через return this.
Примечание: в качестве оптимизации иногда имеет смысл для opAssign
returnconst ref для больших структур. Я не буду применять эту оптимизацию в данной
книге.
В качестве примера оператора, создающего новый объект, определим оператор
opBinary!"+" для оператора Duration. Этот оператор должен сложить два объекта Duration
чтобы создать и вернуть новый объект:

.. code-block:: d

    struct Duration
    {
        int minute;
        Duration opBinary(string op)(Duration rhs) const
        if (op == "+")
        {
            return Duration(minute + rhs.minute); // новый объект
        }
    }


Это определение позволяет добавлять объекты Duration с помощью оператора +:

.. code-block::

  auto travelDuration = Duration(10);
  auto returnDuration = Duration(11);
  Duration totalDuration;
  // ...
  totalDuration = travelDuration + returnDuration;

Компилятор заменяет это выражение следующей функцией-членом
вызываемой на объекте travelDuration:

.. code-block:: d

  // the equivalent of the expression above:
  totalDuration = travelDuration.opBinary!"+"(returnDuration);

*  opDollar
   Поскольку он возвращает количество элементов контейнера, наиболее подходящим
   типом для opDollar является size_t. Однако возвращаемый тип может быть и другим
   также (например, int).
* Нестандартные операторы
  Типы возврата некоторых операторов полностью зависят от конструкции
  определяемого пользователем типа: Унарные \*, opCall, opCast, opDispatch, opSlice,
  и все разновидности opIndex.

opEquals() для сравнения
-------------------------

Эта функция определяет поведение операторов == и !=.
Возвращаемый тип opEquals - bool.
Для структур параметр opEquals может быть определен как in. Однако для скорости
эффективности opEquals может быть определен как шаблон, принимающий auto ref const
(также обратите внимание на пустые скобки шаблона ниже):

.. code-block:: d

  bool opEquals()(auto ref const TimeOfDay rhs) const
  {
  // ...
  }

Как мы уже видели в главе Lvalues и Rvalues (стр. 181), auto ref позволяет
передавать l-значения по ссылке, а r-значения - по копии. Однако, поскольку значения r
не копируются, а перемещаются, приведенная выше сигнатура эффективна как для lvalues, так и для
rvalues.Чтобы избежать путаницы, opEquals и opCmp должны работать согласованно. Для двух
объектов, для которых opEquals возвращает true, opCmp должен возвращать ноль.
Как только opEquals() определена для равенства, компилятор использует ее противоположность для
неравенства:

.. code-block:: d

    x == y;
    // the equivalent of the previous expression:
    x.opEquals(y);
    x != y;
    // the equivalent of the previous expression:
    !(x.opEquals(y));

Обычно нет необходимости определять opEquals() для структур. Компилятор
генерирует ее для структур автоматически. Автоматически генерируемый opEquals
сравнивает все члены по отдельности.
Иногда равенство двух объектов должно быть определено иначе, чем это
автоматического поведения. Например, некоторые члены могут  не важны  при сравнении
или равенство может зависеть от более сложной логики.
В качестве примера давайте определим opEquals() таким образом, чтобы игнорировать минутную
информацию вообще:

.. code-block:: d

    struct TimeOfDay
    {
        int hour;
        int minute;
        bool opEquals(TimeOfDay rhs) const
        {
            return hour == rhs.hour;
        }
    }
    // ...
    assert(TimeOfDay(20, 10) == TimeOfDay(20, 59));

Поскольку при сравнении на равенство учитываются значения только значение часа,
20:10 и 20:59 оказываются равными. (Это просто пример; должно быть понятно.
что такое сравнение равенств приведет к путанице.)

opCmp() для сортировки
----------------------

Операторы сортировки определяют порядок сортировки объектов. Все операторы упорядочивания
<, <=, > и >= охватываются функцией-членом opCmp().
Для структур параметр opCmp может быть определен как in. Однако, как и в случае с
opEquals, эффективнее определить opCmp как шаблон, который принимает auto ref
const

.. code-block:: d

  int opCmp()(auto ref const TimeOfDay rhs) const
  {
  // ...
  }

Чтобы избежать путаницы, opEquals и opCmp должны работать согласованно. Для двух
объектов, для которых opEquals возвращает true, opCmp должен возвращать ноль.
Предположим, что один из этих четырех операторов используется, как в следующем коде:

.. code-block:: d

  if (x op y) { // ← op is one of <, <=, >, or >=

Компилятор преобразует это выражение в следующее логическое выражение и
использует результат нового логического выражения:

.. code-block:: d

  if (x.opCmp(y) op 0) {

Давайте рассмотрим оператор <=:

.. code-block:: d

  if (x <= y) {

За кулисами компилятор генерирует следующий код:

.. code-block:: d

  if (x.opCmp(y) <= 0) {

Чтобы пользовательская функция opCmp() работала правильно, эта функция-член должна
возвращать результат в соответствии со следующими правилами:
* Отрицательное значение, если считается, что левый объект находится перед правым
* Положительное значение, если считается, что левый объект находится после правого
* Ноль, если считается, что объекты имеют одинаковый порядок сортировки.
Чтобы поддерживать эти значения, возвращаемый тип opCmp() должен быть int, а не bool.
Ниже приведен способ упорядочивания объектов TimeOfDay путем сравнения сначала
значения членов часа, а затем сравнивая значения членов минуты
(только если значения часовых членов равны):

.. code-block::

    int opCmp(TimeOfDay rhs) const
    {
    // Note: Subtraction is a bug here if the result can
     * overflow. (See the following warning in text.) */
       return (hour == rhs.hour ? minute - rhs.minute : hour - rhs.hour);
    }

Это определение возвращает разницу между минутными значениями, если значение часа одинаково, и разницу между часами в противном случае.Возвращаемое значение будет отрицательным, если левый объект находится раньшев хронологическом порядке, положительное значение, если правый объект находится раньше, и нольесли они представляют одно и то же время суток.
Предупреждение: Использование вычитания для реализации opCmp является ошибкой, если действительные
значения члена могут привести к переполнению. Например, два объекта, приведенные ниже, будут
будут отсортированы неправильно, так как объект со значением -2 будет вычислен как больший, чем
объект со значением int.max:

.. code-block:: d

    struct S
    {
        int i;
        int opCmp(S rhs) const
        {
            return i - rhs.i; // ← BUG
        }
    }

    void main()
    {
        assert(S(-2) > S(int.max)); // ← wrong sort order
    }

С другой стороны, вычитание допустимо для TimeOfDay, поскольку ни одно из
допустимых значений членов этой структуры не может привести к переполнению при вычитании.Вы можете использовать std.algorithm.cmp для сравнения фрагментов (включая все строковые типы и диапазоны).cmp() сравнивает фрагменты лексикографически и выдает отрицательное значение.отрицательное, нулевое или положительное значение в зависимости от их порядка. Этот результат может
непосредственно использоваться в качестве возвращаемого значения opCmp:

.. code-block:: d

    import std.algorithm;

    struct S
    {
        string name;
        int opCmp(S rhs) const
        {
            return cmp(name, rhs.name);
        }
    }


После определения opCmp() этот тип можно использовать с такими алгоритмами сортировки, как
std.algorithm.sort. Поскольку sort() работает с элементами, именно оператор opCmp()вызывается за кулисами, чтобы определить их порядок. На сайте
следующая программа создает 10 объектов со случайными значениями и сортирует их с помощью
sort():

.. code-block:: d

    import std.random;
    import std.stdio;
    import std.string;
    import std.algorithm;

    struct TimeOfDay
    {
        int hour;
        int minute;
        int opCmp(TimeOfDay rhs) const
        {
            return (hour == rhs.hour ? minute - rhs.minute : hour - rhs.hour);
        }

        string toString() const
        {
            return format("%02s:%02s", hour, minute);
        }
    }

    void main()
    {
        TimeOfDay[] times;
        foreach (i; 0 .. 10)
        {
            times ~= TimeOfDay(uniform(0, 24), uniform(0, 60));
        }
        sort(times);
        writeln(times);
    }


Как и ожидалось, элементы отсортированы от самого раннего времени к самому позднему:

.. code-block::

  [03:40, 04:10, 09:06, 10:03, 10:09, 11:04, 13:42, 16:40, 18:03, 21:08]

opCall() для вызова объектов как функций
----------------------------------------

Круглые скобки вокруг списка параметров при вызове функций также являются операторами
также являются операторами. Мы уже видели, как static opCall() позволяет использовать
имя типа в качестве функции. static opCall() позволяет создавать объекты со значениями
значениями по умолчанию во время выполнения.
Нестатический opCall(), с другой стороны, позволяет использовать объекты пользовательских
типов в качестве функций:

.. code-block:: d

  Foo foo;
  foo();

Объект foo выше вызывается как функция.
В качестве примера рассмотрим структуру, представляющую линейное уравнение. Этот
struct будет использоваться для вычисления значений y следующего линейного уравнения
для определенных значений x:

.. code-block:: d

  y = ax + b

Следующий opCall() просто вычисляет и возвращает значение y в соответствии с
этого уравнения:

.. code-block:: d

  struct LinearEquation
  {
    double a;
    double b;
    double opCall(double x) const
    {
      return a * x + b;
    }
  }

При таком определении каждый объект LinearEquation представляет собой линейное уравнение
для определенных значений a и b. Такой объект может быть использован в качестве функции, которая
вычисляет значения y:

.. code-block:: d

  LinearEquation equation = { 1.2, 3.4 };
  // the object is being used like a function:
  double y = equation(5.6);

*Примечание:Определение функцииopCall() для конструкции отключает автоматический конструктор, созданный компилятором.
конструктор. Именно поэтому выше использован синтаксис{ } вместо рекомендуемого
LinearEquation(1.2, 3.4). Когда требуется последний синтаксис, можно использовать астатический
opCall(), принимающий два двойных параметра, также должен быть определен.*

Приведенное выше уравнение представляет собой линейное уравнение y=1.2x+3.4. Использование этого объекта
в качестве функции приводит к выполнению функции-члена opCall().
Эта возможность может быть полезна для определения и хранения значений a и b в объекте
один раз, а затем использовать этот объект несколько раз. Следующий код использует такой
объект в цикле

.. code-block:: d

    LinearEquation equation = { 0.01, 0.4 };
    for (double x = 0.0; x <= 1.0; x += 0.125)
    {
      writefln("%f: %f", x, equation(x));
    }

Этот объект представляет собой уравнение y=0,01x+0,4. Он используется для вычисления
результатов для значений x в диапазоне от 0,0 до 1,0.

Операторы индексирования
------------------------

opIndex, opIndexAssign, opIndexUnary, opIndexOpAssign и opDollar
позволяют использовать операторы индексации на пользовательских типах, подобных массивам
как в object[index].
В отличие от массивов, эти операторы также поддерживают многомерную индексацию.
Множественные значения индексов указываются в виде списка, разделенного запятыми, внутри квадратных
скобках (например, object[index0, index1]). В следующих примерах мы будем использовать
эти операторы только с одним измерением, а их многомерное
в главе "Дополнительные шаблоны" (стр. 520).
Переменная deque в следующих примерах является объектом struct
DoubleEndedQueue, который мы определим ниже; а e - переменная типа int.
opIndex служит для доступа к элементу. Индекс, указанный внутри скобок
становится параметром операторной функции:

.. code-block:: d

  e = deque[3]; // the element at index 3
  e = deque.opIndex(3); // the equivalent of the above

opIndexAssign предназначен для присвоения элементу. Первым параметром является значение
которое присваивается, а второй параметр - индекс элемента:

.. code-block:: d

  deque[5] = 55; // assign 55 to the element at index 5
  deque.opIndexAssign(55, 5); // the equivalent of the above

opIndexUnary аналогичен opUnary. Разница заключается в том, что операция
применяется к элементу с указанным индексом:

.. code-block:: d

  ++deque[4]; // increment the element at index 4
  deque.opIndexUnary!"++"(4); // the equivalent of the above

opIndexOpAssign аналогичен opOpAssign. Разница в том, что операция
применяется к элементу:

.. code-block:: d

  deque[6] += 66; // add 66 to the element at index 6
  deque.opIndexOpAssign!"+"(66, 6);// the equivalent of the above

opDollar определяет символ $, который используется при индексировании и создании фрагментов. Он используется для возврата количества элементов в контейнере:

.. code-block:: d

  e = deque[$ - 1]; // the last element
  e = deque[deque.opDollar() - 1]; // the equivalent of the above

**Пример операторов индексирования**

Двусторонняя очередь - это структура данных, которая похожа на массивы, но обеспечивает эффективную вставку в начало коллекции. (В отличие от этого, вставка в
в голову массива - относительно медленная операция, поскольку она требует перемещения
существующих элементов во вновь созданный массив).
Один из способов реализации двусторонней очереди - использовать два массива в
но использовать первый из них в обратном порядке. Элемент, который концептуально
вставляется в начало очереди, на самом деле добавляется в головной массив. В результате
В результате эта операция так же эффективна, как и добавление в конец.
Следующая структура реализует двустороннюю очередь, которая перегружает операторы
операторы, которые мы рассматривали в этом разделе:

.. code-block:: d

  import std.stdio;
  import std.string;
  import std.conv;

  struct DoubleEndedQueue // Also known as Deque
  {
  private:
      // The elements are represented as the chaining of the two
      // member slices. However, 'head' is indexed in reverse so
      // that the first element of the entire collection is
      // head[$-1], the second one is head[$-2], etc.:
      //
      // head[$-1], head[$-2], ... head[0], tail[0], ... tail[$-1]


      int[] head; // the first group of elements
      int[] tail; // the second group of elements
      // Determines the actual slice that the specified element
      // resides in and returns it as a reference.
      ref inout(int) elementAt(size_t index) inout
      {
          return (index < head.length ? head[$ - 1 - index] : tail[index - head.length]);
      }

  public:
      string toString() const
      {
          string result;
          foreach_reverse (element; head)
          {
              result ~= format("%s ", to!string(element));
          }

          foreach (element; tail)
          {
              result ~= format("%s ", to!string(element));
          }
          return result;
      }

      // Note: As we will see in the next chapter, the following
      // is a simpler and more efficient implementation of
      // toString():

      version (none)
      {
          void toString(void delegate(const(char)[]) sink) const
          {
              import std.format;
              import std.range;

              formattedWrite(
                  sink, "%(%s %)", chain(head.retro, tail));
          }
      }

      // Adds a new element to the head of the collection. */
      void insertAtHead(int value)
      {
          head ~= value;
      }
      // Adds a new element to the tail of the collection.

      // Sample: deque ~= value

      ref DoubleEndedQueue opOpAssign(string op)(int value) if (op == "~")
      {
          tail ~= value;
          return this;
      }
      // Returns the specified element.
      // Sample: deque[index]

      inout(int) opIndex(size_t index) inout
      {
          return elementAt(index);
      }

      // Applies a unary operation to the specified element.
      // Sample: ++deque[index]


      int opIndexUnary(string op)(size_t index)
      {
          mixin("return " ~ op ~ "elementAt(index);");
      }

      // Assigns a value to the specified element.
      // Sample: deque[index] = value


      int opIndexAssign(int value, size_t index)
      {
          return elementAt(index) = value;
      }

      // Uses the specified element and a value in a binary
      // operation and assigns the result back to the same
      // element.
      // Sample: deque[index] += value


      int opIndexOpAssign(string op)(int value, size_t index)
      {
          mixin("return elementAt(index) " ~ op ~ "= value;");
      }

      // Defines the $ character, which is the length of the
      // collection.
      // Sample: deque[$ - 1]


      size_t opDollar() const
      {
          return head.length + tail.length;
      }
  }

  void main()
  {
      auto deque = DoubleEndedQueue();
      foreach (i; 0 .. 10)
      {
          if (i % 2)
          {
              deque.insertAtHead(i);
          }
          else
          {
              deque ~= i;
          }
      }

      writefln("Element at index 3: %s",
          deque[3]); // accessing an element
      ++deque[4]; // incrementing an element
      deque[5] = 55; // assigning to an element
      deque[6] += 66; // adding to an element
      (deque ~= 100) ~= 200;
      writeln(deque);
  }

Согласно приведенным выше указаниям, возвращаемый тип opOpAssign - ref, чтобы
оператор ~= может быть использован в цепочке на той же коллекции:

.. code-block:: d

  (deque ~= 100) ~= 200;

В результате и 100, и 200 будут добавлены в одну коллекцию:


.. code-block:: d

  Element at index 3: 3
  9 7 5 3 2 55 68 4 6 8 100 200

Операторы фрагментов
---------------------

opSlice позволяет фрагментировать объекты пользовательских типов с помощью оператора [].
Помимо этого оператора, существуют также opSliceUnary, opSliceAssign и
opSliceOpAssign, но использовать их не рекомендуется.
D поддерживает многомерное фоагментирование. Мы рассмотрим пример многомерного фрагментирования
позже в главе "Больше шаблонов" (стр. 520). Хотя методы, описанные
в этой главе, могут быть использованы и для одного измерения, они не соответствуют
операторам индексирования, которые определены выше, и в них используются шаблоны, которые мы
мы еще не рассматривали. По этой причине в этой главе мы рассмотрим нешаблонное использование
opSlice в этой главе, которое работает только с одним измерением. (Такое использование
opSlice также не рекомендуется.)
opSlice имеет две различные формы:

* Квадратные скобки могут быть пустыми, как в deque[], чтобы означать все элементы.
* Квадратные скобки могут содержать диапазон чисел, как в deque[begin..end], чтобы
  означать элементы в указанном диапазоне.

Операторы фрагмнетирования относительно сложнее других операторов, поскольку
они включают в себя два различных понятия: контейнер и диапазон. Мы рассмотрим эти понятия
более подробно в последующих главах.
При одномерном фрагментирование, которое не использует шаблоны, opSlice возвращает
объект, представляющий определенный диапазон элементов контейнера. Объект
который возвращает opSlice, отвечает за определение операций, которые применяются
к элементам этого диапазона. Например, за кулисами следующее выражение
выполняется сначала вызовом opSlice для получения объекта диапазона, а затем применяется
opOpAssign!"*" к этому объекту:

.. code-block:: d

  deque[] *= 10; // multiply all elements by 10
  // The equivalent of the above:
  {
    auto range = deque.opSlice();
    range.opOpAssign!"*"(10);
  }

Соответственно, операторы opSlice в DoubleEndedQueue возвращают специальный объект Range
чтобы операции применялись к нему:

.. code-block:: d

  import std.exception;

  struct DoubleEndedQueue
  {
      // ...
      // Returns a range that represents all of the elements.
      // ('Range' struct is defined below.)
      // Sample: deque[]

      inout(Range) opSlice() inout
      {
          return inout(Range)(head[], tail[]);
      }

      // Returns a range that represents some of the elements.
      // Sample: deque[begin .. end]

      inout(Range) opSlice(size_t begin, size_t end) inout
      {
          enforce(end <= opDollar());
          enforce(begin <= end);
          // Determine what parts of 'head' and 'tail'
          // correspond to the specified range:
          if (begin < head.length)
          {
              if (end < head.length)
              {
                  // The range is completely inside 'head'.
                  return inout(Range)(
                      head[$ - end .. $ - begin],
                      []);
              }
              else
              {
                  // Some part of the range is inside 'head' and
                  // the rest is inside 'tail'.
                  return inout(Range)(
                      head[0 .. $ - begin],
                      tail[0 .. end - head.length]);
              }
          }
          else
          {
              // The range is completely inside 'tail'.
              return inout(Range)(
                  [],
                  tail[begin - head.length .. end - head.length]);
          }
      }

        // Represents a range of elements of the collection. This
        // struct is responsible for defining the opUnary,
        // opAssign, and opOpAssign operators. */
      struct Range
      {
          int[] headRange; // elements that are in 'head'
          int[] tailRange; // elements that are in 'tail'
          // Applies the unary operation to the elements of the
          // range. *
          Range opUnary(string op)()
          {
              mixin(op ~ "headRange[];");
              mixin(op ~ "tailRange[];");
              return this;
          }
          // Assigns the specified value to each element of the
          // range.

          Range opAssign(int value)
          {
              headRange[] = value;
              tailRange[] = value;
              return this;
          }

          // Uses each element and a value in a binary operation
          // and assigns the result back to that element.
          Range opOpAssign(string op)(int value)
          {
              mixin("headRange[] " ~ op ~ "= value;");
              mixin("tailRange[] " ~ op ~ "= value;");
              return this;
          }
      }
  }

  void main()
  {
      auto deque = DoubleEndedQueue();
      foreach (i; 0 .. 10)
      {
          if (i % 2)
          {
              deque.insertAtHead(i);
          }
          else
          {
              deque ~= i;
          }
      }
      writeln(deque);
      deque[] *= 10;
      deque[3 .. 7] = -1;
      writeln(deque);
  }

Вывод

.. code-block:: console

  9 7 5 3 1 0 2 4 6 8
  90 70 50 -1 -1 -1 -1 40 60 80

opCast для преобразования типов
-------------------------------

opCast определяет явные преобразования типов. Он может быть перегружен отдельно для каждого
целевого типа. Как вы помните из предыдущих глав, явные преобразования типов
выполняются функцией to и оператором cast.
opCast - это тоже шаблон, но он имеет другой формат: Целевой тип
задается синтаксисом (T : target_type):

.. code-block:: d

  arget_type opCast(T : target_type)()
  {
  // ...
  }

Этот синтаксис станет понятен позже, в главе о шаблонах.
Давайте изменим определение Duration так, чтобы у него теперь было два члена: часы
и минуты. Оператор, преобразующий объекты этого типа в double, может быть
как в следующем коде:

.. code-block:: d

  import std.stdio;
  import std.conv;

  struct Duration
  {
      int hour;
      int minute;
      double opCast(T : double)() const
      {
          return hour + (to!double(minute) / 60);
      }
  }

  void main()
  {
      auto duration = Duration(2, 30);
      double d = to!double(duration);
      // (could be 'cast(double)duration' as well)
      writeln(d);
  }

Приведенная выше функция двойного преобразования дает 2,5 для двух часов и тридцати
минут:

.. code-block:: console

  2.5

Хотя opCast предназначен для явного преобразования типов, его специализация bool вызывается
автоматически, когда переменная используется в логическом выражении:

.. code-block::

  struct Duration
  {
      // ...
      bool opCast(T : bool)() const
      {
          return (hour != 0) || (minute != 0);
      }
  }

  // ...
  if (duration)
  { // compiles
      // ...
  }

  while (duration)
  { // compiles
      // ...
  }
  auto r = duration ? 1 : 2; // compiles


Тем не менее, bool-специализация opCast предназначена не для всех неявных bool-преобразований:

.. code-block:: d

  void foo(bool b)
  {
  // ...
  }
  // ...

  foo(duration);     // ← ошибка компиляция
  bool b = duration; // ← ошибка компиляция

.. code-block:: console

  Error: cannot implicitly convert expression (duration) of type Duration to bool
  Error: function deneme.foo (bool b) is not callable using argument types (Duration)

Catch-all opDispatch
--------------------

opDispatch вызывается всякий раз, когда происходит обращение к отсутствующему члену объекта. Все
попытки доступа к несуществующим членам передаются этой функции.
Имя отсутствующего члена становится значением шаблонного параметра
opDispatch.
Следующий код демонстрирует простое определение:

.. code-block:: d

  import std.stdio;

  struct Foo
  {
      void opDispatch(string name, T)(T parameter)
      {
          writefln("Foo.opDispatch - name: %s, value: %s", name, parameter);
      }
  }

  void main()
  {
      Foo foo;
      foo.aNonExistentFunction(42);
      foo.anotherNonExistentFunction(100);
  }


Ошибок компилятора при обращении к несуществующим членам не возникает. Вместо этого все
эти вызовы передаются в opDispatch. Первым параметром шаблона является
имя члена. Значения параметров, которые используются при вызове функции
появляются в качестве параметров opDispatch:

.. code-block:: d

  Foo.opDispatch - name: aNonExistentFunction, value: 42
  Foo.opDispatch - name: anotherNonExistentFunction, value: 100

Параметр шаблона имени может быть использован внутри функции для принятия решений
о том, как должен быть обработан вызов конкретной несуществующей функции:

.. code-block:: d

  switch (name)
  {
  // ...
  }

Запрос на проверку вхождения с помощью opBinaryRight!"in"
---------------------------------------------------------

Этот оператор позволяет определить поведение оператора in для пользовательских
типов. in обычно используется с ассоциативными массивами, чтобы определить, существует ли значение
для определенного ключа существует в массиве.
В отличие от других операторов, этот оператор обычно перегружается для случая
для случая, когда объект находится в правой части


.. code-block:: d

  if (time in lunchBreak) {

Компилятор будет использовать opBinaryRight за кулисами:

.. code-block:: d

  // the equivalent of the above:
  if (lunchBreak.opBinaryRight!"in"(time)) {

Также есть возможность определить, существует ли значения для определенного ключа в
массиве:

.. code-block:: d

  if (a !in b) {

!in не может быть перегружен, потому что компилятор использует отрицательный результат оператора
оператора in:

.. code-block:: d

  if (!(a in b)) {

**Пример использования оператора in**

Следующая программа определяет тип TimeSpan в дополнение к Duration и
TimeOfDay. Оператор in, определенный для TimeSpan, определяет, находится ли момент времени
момент времени в пределах этого временного интервала.
Чтобы сделать код коротким, в следующей программе определены только необходимые
функции-члены.
Обратите внимание, как объект TimeOfDay органично используется в цикле for. Этот цикл
демонстрацией того, насколько полезной может быть перегрузка операторов.

.. code-block:: d

  import std.stdio;
  import std.string;

  struct Duration
  {
      int minute;
  }

  struct TimeOfDay
  {
      int hour;
      int minute;
      ref TimeOfDay opOpAssign(string op)(Duration duration) if (op == "+")
      {
          minute += duration.minute;
          hour += minute / 60;
          minute %= 60;
          hour %= 24;
          return this;
      }

      int opCmp(TimeOfDay rhs) const
      {
          return (hour == rhs.hour ? minute - rhs.minute : hour - rhs.hour);
      }

      string toString() const
      {
          return format("%02s:%02s", hour, minute);
      }
  }

  struct TimeSpan
  {
      TimeOfDay begin;
      TimeOfDay end; // end is outside of the span
      bool opBinaryRight(string op)(TimeOfDay time) const
      if (op == "in")
      {
          return (time >= begin) && (time < end);
      }
  }

  void main()
  {
      auto lunchBreak = TimeSpan(TimeOfDay(12, 00),
          TimeOfDay(13, 00));
      for (auto time = TimeOfDay(11, 30); time < TimeOfDay(13, 30); time += Duration(15))
      {
          if (time in lunchBreak)
          {
              writeln(time, " is during the lunch break");
          }
          else
          {
              writeln(time, " is outside of the lunch break");
          }
      }
  }


Вывод

.. code-block:: console

    11:30 is outside of the lunch break
    11:45 is outside of the lunch break
    12:00 is during the lunch break
    12:15 is during the lunch break
    12:30 is during the lunch break
    12:45 is during the lunch break
    13:00 is outside of the lunch break
    13:15 is outside of the lunch break

Упражнение
-----------

Определите тип дроби, который хранит числитель и знаменатель как члены
типа long. Такой тип может быть полезен, потому что он не теряет значения, как float,
double и real из-за их прецизионности. Например, хотя результат
умножения двойного значения 1.0/3 на 3 не будет1.0, умножение объекта Fraction
представляющего дробь 1/3, на 3 будет ровно 1

.. code-block:: d

  struct Fraction
  {
      long num; // numerator
      long den; // denominator
      // As a convenience, the constructor uses the default
      // value of 1 for the denominator.
      this(long num, long den = 1)
      {
          enforce(den != 0, "The denominator cannot be zero");
          this.num = num;
          this.den = den;
          // Ensuring that the denominator is always positive
          // will simplify the definitions of some of the
          // operator functions.

          if (this.den < 0)
          {
              this.num = -this.num;
              this.den = -this.den;
          }
      }

      // ... you define the operator overloads ...
  }


Определите необходимые операторы для этого типа, чтобы сделать его удобным типом, максимально приближенным к
фундаментальным типам, насколько это возможно. Убедитесь, что определение типа проходит все
следующие модульные тесты. Модульные тесты обеспечивают следующее поведение:

* При построении объекта с нулевым знаменателем должно возникать исключение
  знаменателем. (Об этом уже позаботилось выражение enforce выше).
* Получение отрицательного значения: Например, отрицательное значение 1/3 должно быть
  -1/3, а отрицательное значение -2/5 - 2/5.
* Увеличение и уменьшение значения с помощью ++ и --.

Поддержка четырех арифметических операций: Как изменение значения объекта
с помощью операторов +=, -=, \*= и /=, так и получение результата от использования двух объектов с помощью операторов +,
-, \* и /. (Как и в конструкторе, деление на ноль должно быть не допускать).
В качестве напоминания приведем формулы арифметических операций, в которых участвуют
две дроби a/b и c/d:

  * Сложение: a/b + c/d = (a*d + c*b)/(b*d)
  * Вычитание: a/b - c/d = (a*d - c*b)/(b*d)
  * Умножение: a/b * c/d = (a*c)/(b*d)
  * Деление: (a/b) / (c/d) = (a*d)/(b*c)
  * Действительная (и обязательно с потерями) стоимость объекта может быть переведена в
    двойное.

* Сравнения порядка сортировки и равенства выполняются по действительным значениям
  дробей, а не по значениям числителей и знаменателей. Например,
  например, дроби 1/3 и 20/60 должны считаться равными.

.. code-block:: d

    unittest
    {
    // Must throw when denominator is zero.
      assertThrown(Fraction(42, 0));
    // Let's start with 1/3.
      auto a = Fraction(1, 3);
    // -1/3
      assert(-a == Fraction(-1, 3));
    // 1/3 + 1 == 4/3
      ++a;
      assert(a == Fraction(4, 3));
    // 4/3 - 1 == 1/3
      --a;
      assert(a == Fraction(1, 3));
    // 1/3 + 2/3 == 3/3
      a += Fraction(2, 3);
      assert(a == Fraction(1));
    // 3/3 - 2/3 == 1/3
      a -= Fraction(2, 3);
      assert(a == Fraction(1, 3));
    // 1/3 * 8 == 8/3
      a *= Fraction(8);
      assert(a == Fraction(8, 3));
    // 8/3 / 16/9 == 3/2
      a /= Fraction(16, 9);
      assert(a == Fraction(3, 2));
      // Must produce the equivalent value in type 'double'.
      // Note that although double cannot represent every value
      // precisely, 1.5 is an exception. That is why this test
      // is being applied at this point.
      assert(to!double(a) == 1.5);
    // 1.5 + 2.5 == 4
      assert(a + Fraction(5, 2) == Fraction(4, 1));
    // 1.5 - 0.75 == 0.75
      assert(a - Fraction(3, 4) == Fraction(3, 4));
    // 1.5 * 10 == 15
      assert(a * Fraction(10) == Fraction(15, 1));
    // 1.5 / 4 == 3/8
      assert(a / Fraction(4) == Fraction(3, 8));
    // Must throw when dividing by zero.
      assertThrown(Fraction(42, 1) / Fraction(0));
    // The one with lower numerator is before.
      assert(Fraction(3, 5) < Fraction(4, 5));
    // The one with larger denominator is before.
      assert(Fraction(3, 9) < Fraction(3, 8));
      assert(Fraction(1, 1_000) > Fraction(1, 10_000));
    // The one with lower value is before.
      assert(Fraction(10, 100) < Fraction(1, 2));
    // The one with negative value is before.
      assert(Fraction(-1, 2) < Fraction(0));
      assert(Fraction(1, -2) < Fraction(0));
    // The ones with equal values must be both <= and >=.
      assert(Fraction(-1, -2) <= Fraction(1, 2));
      assert(Fraction(1, 2) <= Fraction(-1, -2));
      assert(Fraction(3, 7) <= Fraction(9, 21));
      assert(Fraction(3, 7) >= Fraction(9, 21));
    // The ones with equal values must be equal.
      assert(Fraction(1, 3) == Fraction(20, 60));
    // The ones with equal values with sign must be equal.
      assert(Fraction(-1, 2) == Fraction(1, -2));
      assert(Fraction(1, 2) == Fraction(-1, -2));
  }

:ref:`Решения Перегрузка операторов<Решения Перегрузка операторов>`.
